use std::fmt;

use parser::TextRange;

use crate::file::File;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CompleteDiagnostic {
    pub severity: Severity,
    pub message: String,
    pub sub_diagnostics: Vec<SubDiagnostic>,
    pub notes: Vec<String>,
    pub error_code: GlobalErrorCode,
}

impl CompleteDiagnostic {
    pub fn new(
        severity: Severity,
        message: String,
        sub_diagnostics: Vec<SubDiagnostic>,
        notes: Vec<String>,
        error_code: GlobalErrorCode,
    ) -> Self {
        Self {
            severity,
            message,
            sub_diagnostics,
            notes,
            error_code,
        }
    }

    pub fn primary_span(&self) -> Span {
        self.sub_diagnostics
            .iter()
            .find_map(|sub| sub.is_primary().then(|| sub.span.clone().unwrap()))
            .unwrap()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GlobalErrorCode {
    pub pass: DiagnosticPass,
    pub local_code: u16,
}

impl GlobalErrorCode {
    pub fn new(pass: DiagnosticPass, local_code: u16) -> Self {
        Self { pass, local_code }
    }
}

impl fmt::Display for GlobalErrorCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}-{:04}", self.pass.code(), self.local_code)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SubDiagnostic {
    pub style: LabelStyle,
    pub message: String,
    pub span: Option<Span>,
}

impl SubDiagnostic {
    pub fn new(style: LabelStyle, message: String, span: Option<Span>) -> Self {
        Self {
            style,
            message,
            span,
        }
    }

    pub fn is_primary(&self) -> bool {
        matches!(self.style, LabelStyle::Primary)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LabelStyle {
    Primary,
    Secondary,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Span {
    pub file: File,
    pub range: TextRange,
    pub kind: SpanKind,
}

impl PartialOrd for Span {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Span {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        match self.file.cmp(&other.file) {
            std::cmp::Ordering::Equal => self.range.start().cmp(&other.range.start()),
            ord => ord,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpanKind {
    /// A node corresponding is originally written in the source code.
    Original,

    /// A node corresponding to the span is generated by macro expansion.
    Expanded,

    /// No span information was found.
    /// This happens if analysis code tries to get a span for a node that is
    /// generated in lowering phase.
    ///
    /// If span has this kind, it means there is a bug in the analysis code.
    /// The reason not to panic is that LSP should continue working even if
    /// there are bugs in the span generation(This also makes easier to identify
    /// the cause of the bug)
    ///
    /// Range is always the first character of the file in this case.
    NotFound,
}

impl Span {
    pub fn new(file: File, range: TextRange, kind: SpanKind) -> Self {
        Self { file, range, kind }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Severity {
    Error,
    Warning,
    Note,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum DiagnosticPass {
    Parse,

    NameResolution,

    TypeDefinition,
    TraitDefinition,
    ImplTraitDefinition,
    TraitSatisfaction,
    MethodDefinition,
    TyCheck,

    ExternalAnalysis(ExternalAnalysisKey),
}

impl DiagnosticPass {
    pub fn code(&self) -> u16 {
        match self {
            Self::Parse => 1,
            Self::NameResolution => 2,
            Self::TypeDefinition => 3,
            Self::TraitDefinition => 4,
            Self::ImplTraitDefinition => 5,
            Self::TraitSatisfaction => 6,
            Self::MethodDefinition => 7,
            Self::TyCheck => 8,

            Self::ExternalAnalysis(_) => u16::MAX,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExternalAnalysisKey {
    name: String,
}
